package com.github_Dididipapa.shortlink.project.service.impl;


import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github_Dididipapa.shortlink.project.common.constant.RabbitConstant;
import com.github_Dididipapa.shortlink.project.common.enums.VailDateTypeEnum;
import com.github_Dididipapa.shortlink.project.common.exception.ClientException;
import com.github_Dididipapa.shortlink.project.dao.entity.*;
import com.github_Dididipapa.shortlink.project.dao.mapper.*;
import com.github_Dididipapa.shortlink.project.dto.req.*;
import com.github_Dididipapa.shortlink.project.dto.resp.*;
import com.github_Dididipapa.shortlink.project.feign.ThirdPartyFeign;
import com.github_Dididipapa.shortlink.project.remote.resp.IPLocationRespDTO;
import com.github_Dididipapa.shortlink.project.service.*;

import com.github_Dididipapa.shortlink.project.toolkit.CacheExpireUtil;
import com.github_Dididipapa.shortlink.project.toolkit.HashUtil;
import com.github_Dididipapa.shortlink.project.toolkit.LinkUtil;
import com.github_Dididipapa.shortlink.project.toolkit.RandomCodeGenerator;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * @description: To be defined
 * @author: dididi_papa
 * @email: mitis1624087624@gmail.com
 * @date: 2025/09/12  11:17
 */
@Service
@RequiredArgsConstructor
public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements LinkService {

    @Resource(name = "RBloom-link")
    private final RBloomFilter<String> bloomFilter;
    private final RabbitTemplate rabbitTemplate;
    private final RedissonClient redissonClient;
    private final StringRedisTemplate template;
    private final LinkMapper linkMapper;
    private final LinkGoToMapper linkGoToMapper;
    private final ThirdPartyFeign thirdPartyFeign;
    private final AsyncService asyncService;

    private String getFavicon(String url) {
        try {
            Document doc = Jsoup.connect(url).get();
            // 查找<link>标签中rel属性包含"icon"的元素
            for (Element link : doc.select("link[rel~=(?i)icon]")) {
                String faviconUrl = link.attr("href");
                // 处理相对路径
                if (!faviconUrl.startsWith("http")) {
                    faviconUrl = url + faviconUrl; // 这里可以根据需要调整
                }
                return faviconUrl;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "未找到favicon";
    }

    private void shortLinkAccessStats(String fullShortUrl,
                                      String gid,
                                      ServletRequest request,
                                      ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        AtomicBoolean uipFirstFlag = new AtomicBoolean();
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        // 获取cookie
        Cookie[] cookies = httpServletRequest.getCookies();
        // 获取ip
        String addr = LinkUtil.getIp(httpServletRequest);
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 14);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            template.opsForSet().add("link:stats:uv" + fullShortUrl, uv.get());
            httpServletResponse.addCookie(uvCookie);
        };

        try {
            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies)
                        .filter(cookie -> Objects.equals(cookie.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(value -> {
                                    uv.set(value);
                                    Long uvAdd = template.opsForSet().add("link:stats:uv" + fullShortUrl, value);
                                    Long uipAdd = template.opsForSet().add("link:stats:uip" + fullShortUrl, addr);
                                    uvFirstFlag.set(uvAdd != null && uvAdd > 0L);
                                    uipFirstFlag.set(uipAdd != null && uipAdd > 0L);
                                }, addResponseCookieTask
                        );
            }
            else {
                addResponseCookieTask.run();
            }

            if (!StringUtils.hasText(gid)) {
                LambdaQueryWrapper<LinkGoTo> queryWrapper = Wrappers.lambdaQuery(LinkGoTo.class)
                        .eq(LinkGoTo::getFullShortUrl, fullShortUrl);
                LinkGoTo linkGoTo = linkGoToMapper.selectOne(queryWrapper);
                gid = linkGoTo.getGid();
            }

            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getValue();

            //accessStats处理
            AccessStats stats = AccessStats.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .uip(uipFirstFlag.get() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new DateTime())
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_ACCESS_STATS, stats.toString().getBytes());
            //accessStatsMapper.shortLinkAccessStats(stats);

            //localStats处理
            IPLocationRespDTO data = thirdPartyFeign.getLocation(addr).getData();
            LocalStats localStats = LocalStats.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .province(data.getProvince())
                    .city(data.getCity())
                    .adcode(data.getAdcode())
                    .date(new DateTime())
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_LOCAL_STATS, localStats.toString().getBytes());
            //localStatsService.updateDetil(localStats);

            //osStats处理
            String osName = LinkUtil.getOs(httpServletRequest);
            OsStats osStats = OsStats.builder()
                    .fullShortUrl(fullShortUrl)
                    .os(osName)
                    .gid(gid)
                    .createTime(new Date())
                    .updateTime(new Date())
                    .delFlag(0)
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_OS_STATS, osStats.toString().getBytes());
            //osStatsService.updateDetil(osStats);

            //browserStats处理
            String browserName = LinkUtil.getBrowser(httpServletRequest);
            LinkBrowserStats build = LinkBrowserStats.builder()
                    .fullShortUrl(fullShortUrl)
                    .browser(browserName)
                    .gid(gid)
                    .createTime(new Date())
                    .updateTime(new Date())
                    .delFlag(0)
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_BROWSER_STATS, build.toString().getBytes());
            //linkBrowserStatsService.updateDetil(build);

            //linkAccessLogs处理
            LinkAccessLogs linkAccessLogs = LinkAccessLogs.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .ip(addr)
                    .browser(browserName)
                    .user(uv.get())
                    .os(osName)
                    .createTime(new Date())
                    .updateTime(new Date())
                    .delFlag(0)
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_LINK_ACCESS_LOGS, linkAccessLogs.toString().getBytes());
            //linkAccessLogsService.getBaseMapper().insert(linkAccessLogs);

            //linkDeviceStats处理
            LinkDeviceStats linkDeviceStatsDO = LinkDeviceStats.builder()
                    .device(LinkUtil.getDevice(((HttpServletRequest) request)))
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_LINK_DEVICE_STATS, linkDeviceStatsDO.toString().getBytes());
            //linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);

            //linkNetworkStats处理
            LinkNetworkStats linkNetworkStatsDO = LinkNetworkStats.builder()
                    .network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            rabbitTemplate.convertAndSend(RabbitConstant.EXCHANGE_NAME, RabbitConstant.QUEUE_LINK_NETWORK_STATS, linkNetworkStatsDO.toString().getBytes());
            //linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

        } catch (Throwable e) {
            log.error("统计访问数据失败", e);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public LinkCreateRespDTO createLink(LinkReqDTO linkReqDTO) {
        Link link = new Link();
        BeanUtils.copyProperties(linkReqDTO, link);
        if (link.getGid().isEmpty()) link.setGid(RandomCodeGenerator.generateRandomCode());
        link.setEnableStatus(0);
        link.setDelFlag(0);
        link.setFavicon(getFavicon(link.getOriginUrl()));
        link.setShortUri(HashUtil.hashToBase62(link.getOriginUrl()));
        link.setFullShortUrl(link.getDomain() + "/" + link.getShortUri());
        link.setCreateTime(new Date());
        link.setUpdateTime(new Date());
        RReadWriteLock lock = redissonClient.getReadWriteLock("shortLink" + link.getGid());
        int maxRetries = 5;
        int retryCount = 0;

        while (retryCount < maxRetries) {
            lock.readLock().lock();
            boolean exists;
            try {
                exists = linkMapper.exists(new LambdaQueryWrapper<Link>().eq(Link::getShortUri, link.getShortUri()));
            } finally {
                lock.readLock().unlock();
            }
            if (!exists) break;

            // 发生冲突，重新生成 hash
            link.setShortUri(HashUtil.hashToBase62(link.getOriginUrl() + System.nanoTime()));
            retryCount++;
        }

        if (retryCount >= maxRetries) {
            throw new ClientException("短链接生成失败，重试次数已达上限");
        }
        lock.writeLock().lock();
        try {
            // 再次确认数据库中不存在该短链接（双重检查）
            if (linkMapper.exists(new LambdaQueryWrapper<Link>().eq(Link::getShortUri, link.getShortUri()))) {
                throw new ClientException("短链接已存在");
            }
            // 添加到布隆过滤器
            try {
                bloomFilter.add(link.getShortUri());
            } catch (Exception e) {
                log.warn("BloomFilter 添加失败");
            }
            linkMapper.insert(link);
            // 对新创建的短链接进行缓存预热
            template.opsForValue().set(
                    link.getShortUri(),
                    link.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(link.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
            LinkGoTo linkGoTo = new LinkGoTo(link.getId(), link.getGid(), link.getFullShortUrl());
            linkGoToMapper.insert(linkGoTo);
        } catch (Exception e) {
            throw new ClientException("创建短链失败");
        } finally {
            lock.writeLock().unlock();
        }
        return LinkCreateRespDTO.builder()
                .gid(link.getGid())
                .originUrl(link.getOriginUrl())
                .fullShortUrl(link.getFullShortUrl())
                .build();
    }


    @Override
    public IPage<PageRespDTO> pageLink(PageReqDTO requestParam) {
        RReadWriteLock lock = redissonClient.getReadWriteLock("shortLink" + requestParam.getGid());
        lock.readLock().lock();
        IPage<Link> resultPage = null;
        try {
            LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<Link>()
                    .eq(Link::getGid, requestParam.getGid())
                    .eq(Link::getEnableStatus, 1)
                    .eq(Link::getDelFlag, 0)
                    .orderByDesc(Link::getCreateTime);

            IPage<Link> linkPage = new Page<>(requestParam.getCurrent(), requestParam.getSize());
            resultPage = linkMapper.selectPage(linkPage, queryWrapper);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.readLock().unlock();
        }

        List<PageRespDTO> pageRespDTOS = resultPage.getRecords().stream().map(link -> {
            PageRespDTO pageRespDTO = new PageRespDTO();
            BeanUtils.copyProperties(link, pageRespDTO);
            return pageRespDTO;
        }).collect(Collectors.toList());

        IPage<PageRespDTO> page = new Page<>(requestParam.getCurrent(), requestParam.getSize());
        page.setRecords(pageRespDTOS);
        page.setTotal(resultPage.getTotal());
        page.setPages(resultPage.getPages());
        return page;
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateLink(LinkUpdateReqDTO linkReqDTO) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("shortLink" + linkReqDTO.getGid());
        readWriteLock.readLock().lock();
        LambdaQueryWrapper<Link> queryWrapper = null;
        Link link = null;
        try {
            queryWrapper = new LambdaQueryWrapper<Link>()
                    .eq(Link::getGid, linkReqDTO.getOriginGid())
                    .eq(Link::getFullShortUrl, linkReqDTO.getFullShortUrl())
                    .eq(Link::getDelFlag, 0)
                    .eq(Link::getEnableStatus, 0);

            link = linkMapper.selectOne(queryWrapper);
            if(link == null) throw new ClientException("");
        } catch (ClientException e) {
            throw new ClientException("短链接记录不存在");
        }finally {
            readWriteLock.readLock().unlock();
        }

        readWriteLock.writeLock().lock();
        try {
            //相等说明gid没有发生变化， 在物理表中还是同一个记录
            if(Objects.equals(link.getGid(), linkReqDTO.getGid())) {
                LambdaUpdateWrapper<Link> updateWrapper = new LambdaUpdateWrapper<Link>()
                        .eq(Link::getGid, linkReqDTO.getGid())
                        .eq(Link::getFullShortUrl, linkReqDTO.getFullShortUrl())
                        .eq(Link::getDelFlag, 0)
                        .eq(Link::getEnableStatus, 0)
                        .set(Link::getOriginUrl, linkReqDTO.getOriginUrl())
                        .set(Link::getCreatedType, linkReqDTO.getCreatedType())
                        .set(Link::getValidDateType, linkReqDTO.getValidDateType())
                        .set(Link::getDescribe, linkReqDTO.getDescribe())
                        .set(Link::getUpdateTime, new Date());
                // 有效时间类型为无限时，有效时间设置为null
                if (linkReqDTO.getValidDateType() != VailDateTypeEnum.PERMANENT.getCode()) {
                    updateWrapper.set(Link::getValidDate, linkReqDTO.getValidDate());
                }
                else updateWrapper.set(Link::getValidDate, null);

                Link build = Link.builder()
                        .gid(linkReqDTO.getGid())
                        .originUrl(linkReqDTO.getOriginUrl())
                        .describe(linkReqDTO.getDescribe())
                        .validDateType(linkReqDTO.getValidDateType())
                        .validDate(linkReqDTO.getValidDate())
                        .build();
                linkMapper.update(build, updateWrapper);
            }
            // gid有变化，需要先删除逻辑表中的记录，再插入新的记录
            else {
                linkMapper.delete(queryWrapper);
                //修改gid为最新的gid
                link.setGid(linkReqDTO.getGid());
                LambdaQueryWrapper<LinkGoTo> linkGoToLambdaQueryWrapper = Wrappers.lambdaQuery(LinkGoTo.class)
                        .eq(LinkGoTo::getFullShortUrl, link.getFullShortUrl());
                LinkGoTo linkGoTo = linkGoToMapper.selectOne(linkGoToLambdaQueryWrapper);
                linkGoTo.setGid(linkReqDTO.getGid());
                linkGoToMapper.update(linkGoTo, linkGoToLambdaQueryWrapper);
                linkMapper.insert(link);

                // 缓存更新
                template.opsForHash().delete("short-link-map", linkReqDTO.getFullShortUrl());
                template.opsForHash().put("short-link-map", link.getFullShortUrl(), link.getOriginUrl());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    @Transactional
    @Override
    public void createLinkBatch(LinkBatchReqDTO linkBatchReqDTO) {
        RLock lock = redissonClient.getLock("linkBatchCreate");
        lock.lock();
        try {
            // 先查询出所有的短链接后缀
            Set<String> shortUrlSuffix = linkMapper.setOfShortUri();
            int size = linkBatchReqDTO.getOriginUrls().size();
            ArrayList<Link> links = Lists.newArrayList();

            for (int i = 0; i < size; i++) {
                String uri = HashUtil.hashToBase62(linkBatchReqDTO.getOriginUrls().get(i));
                boolean exists = shortUrlSuffix.contains(uri);
                while (exists) {
                    uri = HashUtil.hashToBase62(linkBatchReqDTO.getOriginUrls().get(i));
                    exists = shortUrlSuffix.contains(uri);
                }
                links.add(Link.builder()
                        .gid(RandomCodeGenerator.generateRandomCode())
                        .shortUri(uri)
                        .enableStatus(0)
                        .clickNum(0)
                        .fullShortUrl(linkBatchReqDTO.getDomain() + "/" + uri)
                        .originUrl(linkBatchReqDTO.getOriginUrls().get(i))
                        .describe(linkBatchReqDTO.getDescribes().get(i))
                        .createdType(linkBatchReqDTO.getCreatedType())
                        .validDateType(linkBatchReqDTO.getValidDateType())
                        .validDate(linkBatchReqDTO.getValidDate())
                        .build()
                );
            }
            linkMapper.linkInsertBatch(links);

            //linkgoto 表插入
            List<LinkGoTo> linkGoTos = links.stream()
                    .map(link -> LinkGoTo.builder().gid(link.getGid()).fullShortUrl(link.getFullShortUrl()).build())
                    .collect(Collectors.toList());
            linkGoToMapper.linkgotoInsertBatch(linkGoTos);
        } catch (Exception e) {
            throw new ClientException("批量创建短链接失败");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public String getTitle(String url) {
        LambdaQueryWrapper<Link> wrapper = new LambdaQueryWrapper<Link>()
                .eq(Link::getOriginUrl, url)
                .or()
                .eq(Link::getFullShortUrl, url)
                .or()
                .eq(Link::getShortUri, url);
        Link link = linkMapper.selectOne(wrapper);
        if (link != null) {
            return link.getDescribe();
        }
        return null;
    }

    @Transactional
    @Override
    public void recycleBinSave(RecycleBinLinkReqDTO recycleBinLinkReqDTO) {
        LambdaQueryWrapper<Link> wrapper = new LambdaQueryWrapper<Link>()
                .eq(Link::getGid, recycleBinLinkReqDTO.getGid())
                .eq(Link::getFullShortUrl, recycleBinLinkReqDTO.getFullShortUrl())
                .eq(Link::getDelFlag, 0);
        Link link = linkMapper.selectOne(wrapper);
        if (link == null) return;

        link.setEnableStatus(0);
        linkMapper.update(link, wrapper);
        template.delete(link.getFullShortUrl());
        template.opsForValue().set(
                "recycleLink_" + link.getFullShortUrl(),
                link.getOriginUrl(),
                CacheExpireUtil.generateExpireTime(3600 * 24, 3600 * 24 * 7)
        );
        //延迟双删
        asyncService.deleteRedisCache(link.getFullShortUrl(), 100);
    }

    @Override
    public IPage<RecyleBinRespDTO> pageRecycleBinLink(RecycleBinPageReqDTO requestParam) {
        List<Link> recycleLink = linkMapper.selectList(new LambdaQueryWrapper<Link>()
                .eq(Link::getDelFlag, 0)
                .eq(Link::getEnableStatus, 1));

        List<RecyleBinRespDTO> result = recycleLink
                .stream().filter(Objects::nonNull).map(item -> {
            RecyleBinRespDTO recyleBinRespDTO = new RecyleBinRespDTO();
            JSONObject jsonObject = JSON.parseObject(item.toString());
            BeanUtils.copyProperties(jsonObject, recyleBinRespDTO);
            return recyleBinRespDTO;
        }).collect(Collectors.toList());

        IPage<RecyleBinRespDTO> page = new Page<>();
        page.setRecords(result);
        page.setTotal(result.size());
        page.setCurrent(requestParam.getCurrent());
        page.setSize(requestParam.getSize());
        return page;
    }

    @Transactional
    @Override
    public void recoverLink(RecycleBinLinkReqDTO recycleBinLinkReqDTO) {
        LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<Link>()
                .eq(Link::getGid, recycleBinLinkReqDTO.getGid())
                .eq(Link::getDelFlag, 0)
                .eq(Link::getFullShortUrl, recycleBinLinkReqDTO.getFullShortUrl());

        Link link = linkMapper.selectOne(queryWrapper);

        if(link != null) {
            link.setEnableStatus(1);
            linkMapper.updateById(link);
            template.delete("recycleLink_" + link.getFullShortUrl());
            //重新加入缓存
            template.opsForHash().put("short-link-map", link.getShortUri(), link.getOriginUrl());
            asyncService.deleteRedisCache("recycleLink_" + link.getFullShortUrl(), 100);
        }
    }

    @Override
    public void removeLink(RecycleBinLinkReqDTO recycleBinLinkReqDTO) {
        LambdaQueryWrapper<Link> queryWrapper = new LambdaQueryWrapper<Link>()
                .eq(Link::getGid, recycleBinLinkReqDTO.getGid())
                .eq(Link::getDelFlag, 0)
                .eq(Link::getEnableStatus, 1)
                .eq(Link::getFullShortUrl, recycleBinLinkReqDTO.getFullShortUrl());

        Link link = linkMapper.selectOne(queryWrapper);
        link.setDelFlag(1);
        linkMapper.updateById(link);
        template.opsForHash().delete("short-link-map", link.getShortUri());
    }

    @Override
    public List<LinkGroupCountRespDTO> listGroupShortLinkCount(List<String> reqestParams) {
        Map<String, Integer> map = linkMapper.listGroupShortLinkCount(reqestParams);
        return map.entrySet().stream().map(item -> {
            LinkGroupCountRespDTO linkGroupCountRespDTO = new LinkGroupCountRespDTO();
            linkGroupCountRespDTO.setGid(item.getKey());
            linkGroupCountRespDTO.setShortLinkCount(item.getValue());
            return linkGroupCountRespDTO;
        }).collect(Collectors.toList());
    }

    @SneakyThrows
    @Override
    public void routerLink(String shortLinkUri,
                           ServletRequest request,
                           ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        if(bloomFilter.contains(shortLinkUri)) {
            //String protocol = httpServletRequest.getScheme();
            String serverName = request.getServerName();
            String fullShortLink = serverName + "/" + shortLinkUri;
            if (fullShortLink.endsWith("/")) fullShortLink = fullShortLink.substring(0, fullShortLink.length() - 1);

            String validate = template.opsForValue().get("validate_" + fullShortLink);
            if(!StringUtils.hasText(validate)) {
                httpServletResponse.sendRedirect("/page/notfound");
                return;
            }

            // 查询短链是否在回收站
            if (Objects.requireNonNull(template.opsForValue().get("recycleLink_" + fullShortLink)).isEmpty()) {
                httpServletResponse.sendRedirect("/page/notfound");
                return;
            }

            // 查询短链和跳转地址缓存
            String firstRedirectUrl = template.opsForValue().get(fullShortLink);
            if (StringUtils.hasText(firstRedirectUrl)) {
                shortLinkAccessStats(fullShortLink, null, request, response);
                httpServletResponse.sendRedirect(firstRedirectUrl);
                return;
            }

            RLock rLock = redissonClient.getLock("link-router-lock");
            rLock.lock();
            try {
                // 双检判断短链是否存在
                String secondRedirectUrl = template.opsForValue().get(fullShortLink);
                if (StringUtils.hasText(secondRedirectUrl)) {
                    bloomFilter.add(shortLinkUri);
                    shortLinkAccessStats(fullShortLink, null, request, response);
                    httpServletResponse.sendRedirect(secondRedirectUrl);
                    return;
                }

                LambdaQueryWrapper<LinkGoTo> linkGoToLambdaQueryWrapper = Wrappers.lambdaQuery(LinkGoTo.class)
                        .eq(LinkGoTo::getFullShortUrl, fullShortLink);
                LinkGoTo linkGoTo = linkGoToMapper.selectOne(linkGoToLambdaQueryWrapper);
                if (linkGoTo == null) {
                    // 生成环境需要加入风险控制
                    log.error("短链不存在");
                    httpServletResponse.sendRedirect("/page/notfound");
                    return;
                }

                RReadWriteLock lock = redissonClient.getReadWriteLock("shortLink" + linkGoTo.getGid());
                lock.readLock().lock();
                Link link = null;
                try {
                    LambdaQueryWrapper<Link> linkQueryWrapper = Wrappers.lambdaQuery(Link.class)
                            .eq(Link::getGid, linkGoTo.getGid())
                            .eq(Link::getFullShortUrl, fullShortLink)
                            .eq(Link::getDelFlag, 0)
                            .eq(Link::getEnableStatus, 0);
                    link = linkMapper.selectOne(linkQueryWrapper);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.readLock().unlock();
                }

                lock.writeLock().lock();
                try {
                    if (link != null) {
                        if (link.getValidDate() != null && link.getValidDate().before(new Date())) {
                            // 过期的短链接缓存设置，防止缓存穿透或恶意访问
                            template.opsForValue().set("validate_" + fullShortLink, "", CacheExpireUtil.generateExpireTime(3600 * 24, 3600 * 24 * 7));
                            httpServletResponse.sendRedirect("/page/notfound");
                            return;
                        }
                        else if(link.getEnableStatus() == 1) {
                            // 禁用的短链接缓存设置，防止缓存穿透或恶意访问
                            template.opsForValue().set("recycleLink_" + fullShortLink, "", CacheExpireUtil.generateExpireTime(3600 * 24, 3600 * 24 * 7));
                            httpServletResponse.sendRedirect("/page/notfound");
                            return;
                        }
                        template.opsForValue().set(fullShortLink, link.getOriginUrl(), CacheExpireUtil.generateExpireTime(3600 * 24, 3600 * 24 * 7));
                        link.setClickNum(link.getClickNum() + 1);
                        linkMapper.updateById(link);
                        shortLinkAccessStats(fullShortLink, link.getGid(), request, response);
                        httpServletResponse.sendRedirect(link.getOriginUrl());
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.writeLock().unlock();
                }
            } catch (RuntimeException e) {
                throw new RuntimeException(e);
            } finally {
                rLock.unlock();
            }
        }
        else {
            httpServletResponse.sendRedirect("/page/notfound");
        }
    }
}
